
Timer 제작 과정
setInterval → useInterval → Web Worker
TimeLog에 사용할 타이머를 만드는 과정에서 겪은 경험을 공유하고자 합니다.
처음은 누구라도 그러하듯이 setInterval로 타이머를 만들기 시작했습니다.
하지만 타이머가 제대로 작동하지 않아 검색을 해보니, setInterval에 몇 가지 문제가 있는데,
그 해결책으로 useInterval이라는 custom Hook을 발견했습니다. (문제에 관한 내용은 아래 참고 링크에 있습니다)
따라서 useInterval로 바꾸었고, 타이머가 작동하여 뿌듯한 기분으로 다른 작업을 하였습니다.
하지만 저의 뿌듯함을 비웃듯 타이머에 또 다른 문제가 생겼습니다.
타이머를 오래 작동하면 타이머에 지연이 생기는 것이었습니다.
이번에도 검색을 통해 문제의 원인을 찾아보았습니다.
- CPU가 과부하 상태인 경우
 - 브라우저 탭이 백그라운드 모드인 경우
 - 노트북이 배터리에 의존해서 구동 중인 경우
 
위의 경우 브라우저 내 타이머가 느려져 setInterval의 지연 간격이 길어진다는 것이었습니다. (useInterval도 setInterval을 사용합니다)
경우 추측
크롬은 메모리가 부족하면 비활성 탭이 절전 되는 기능인, “Automatic tab discarding”이 내장되어 있습니다.
“chrome://flags”에서 Automatic tab discarding을 조절할 수 있었지만 없어졌고,
“chrome://discards”에 접속하면 모든 탭에 Auto Discardable이 활성화 되어 있는 것을 확인할 수 있습니다.
(참고로 Graph탭에서 Web Workers를 확인할 수도 있습니다)
이 기능으로 인해 탭이 절전 됐거나, 다른 내장 기능에 의해 자동으로 백그라운드 모드에 진입해서 지연된 것 같습니다.
이를 해결하기 위한 무수한 검색 중에 Web Worker를 발견합니다.
Web Worker는 웹의 Main thread와 별개로, Background thread에서 script를 실행하는 기술입니다.
따라서 Web Worker script에서 setInterval을 사용하면 브라우저 내 타이머 지연과 상관없이 정상 작동합니다.
그러니 어서 사용합시다.
React & TypeScript
// tsconfig.json
{
  "compilerOptions": {
    ...
    "lib": [ ... "WebWorker"], // Worker Scope에서 쓰이는 타입 추가
    ...
    }
}// worker.ts
const self = globalThis as unknown as DedicatedWorkerGlobalScope; // Double assertion
let time = 0;
self.onmessage = () => {
  setInterval(() => {
    time++;
    self.postMessage(time);
  }, 100);
};
export {}; // --isolatedModules 에러 방지 (모듈화)
// Timer.tsx
function Timer() {
  const [time, setTime] = useState(0);
  const [timerOn, setTimerOn] = useState(false);
  const timeSecond = Math.floor(time / 10);
  const timerStart = () => {
    setTimerOn(true);
  };
  const timerStop = () => {
    setTimerOn(false);
  };
  useEffect(() => {
    const worker = new Worker(new URL('worker/worker', import.meta.url)); // webpack5 이후 용법
    if (timerOn === true) {
      worker.postMessage('timer start');
      worker.onmessage = (e: MessageEvent<string>) => {
        setTime(time + Number(e.data));
      };
    }
    return () => {
      worker.terminate();
    };
  }, [timerOn]);
  return (
    <div>
      <div className='timer'>{timeSecond}</div>
      <div>
        <button onClick={timerStart}>Start</button>
        <button onClick={timerStop}>Stop</button>
      </div>
    </div>
  );
}
export default Timer;동작 과정
- 
Background thread에서 실행할 Worker sciprt를 worker.ts에 작성합니다. (이하 worker)
 - 
Timer.tsx(main script)에서
new Worker()로 worker를 실행합니다.- timerOn 값이 바뀔 때마다 그 전에 
worker.terminate()로 현재 worker가 종료되고, 이후 새로운 worker를 실행합니다. 
 - timerOn 값이 바뀔 때마다 그 전에 
 - 
타이머 버튼의 클릭 이벤트로 timerOn 값이 true가 되면,
worker.postMessage()로 worker에 메세지 데이터를 전달합니다.- 여기서 메세지 데이터는 사용하지 않으므로 중요하지 않습니다.
 
 - 여기서 메세지 데이터는 사용하지 않으므로 중요하지 않습니다.
 - 
worker의
self.onmessage에 적힌 동작들이 실행됩니다. (setInterval 실행) - 
Timer.tsx에서
worker.onmessage로, worker에서self.postMessage()로 보낸 time을 100ms마다 기존 time에 추가합니다.